在前一篇文章中,我們完成了一個 Coroutine 的程式,並且在最後我們發現了兩個特點:
除了上述這兩點,我認為 Coroutine 有四個特點:
下面我們就依序來看一下這幾個特點吧,
前面提到,我們可以使用 Callback 來處理非同步的運算,但是這樣就讓程式碼變得不容易維護,因為如果 Callback 的層次過多,會發生 Callback hell 的情況。另外,當使用 Callback 時,我們等於是把控制權交給上一個函式,也就是控制權轉移,假如上一個函式如果沒有妥善呼叫 Callback ,反而會造成誤動作。(可以參考上一篇文章的介紹)
將程式碼採用 Coroutine 的方式來完成,可以讓非同步的程式碼的也可以像是同步的程式碼一樣呼叫,自然也就不會有 Callback hell 以及控制權轉移的情況了。
「可看作」的意思就是不是完全一樣。
官網的介紹如下:
Coroutines can be thought of as light-weight threads, but there is a number of important differences that make their real-life usage very different from threads. - Ref
使用 Coroutine 來解決非同步程式的執行問題,且因為它很輕量,所以我們可以同時建立很多個 Coroutine 也不會造成執行緒的阻塞。
val threadExecuteTime = measureTimeMillis {
repeat(100_000) {
thread {
// Do nothing
}.start()
}
}
println("Completed, duration: $threadExecuteTime ms")
//Completed, duration: 17187 ms
thread{}
是 Kotlin 中用來建立執行緒的方法。
val coroutinesExecuteTime = measureTimeMillis {
runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
//Do nothing
}
}
}
}
println("Completed, duration: $coroutinesExecuteTime ms")
//Completed, duration: 493 ms
launch{}
用來建立新的 Coroutine 以及 Scope。
建立同樣數量的 Coroutines 以及 Threads,Coroutine 的執行時間比 Thread 的執行時間要來得少。(493ms vs 17187ms)
預設的情況之下,每一個應用程式 (Application) 有一個程序 (Process) 以及一個執行緒 (Thread),稱之為主執行緒。程序會佔用系統的一塊記憶區塊,所有該應用程式的程式碼以及相關資源都會與其他應用程式的隔離開來,只有自己應用程式才能使用自己記憶區塊的內容。執行緒在程序的底下根據分配到的 CPU 時間來執行,如果只有一個執行緒,當有一個耗時工作要處理時,就會一直把取得的 CPU 時間拿去運算,造成畫面卡住。所以我們會使用主執行緒以外的執行緒來進行耗時運算。
從上方的圖我們可以發現,每一個執行緒都可以擁有多個 Coroutine ,也就是說 Coroutine 把執行緒分為更小單位。所以 Coroutine 可以稱得上是輕量級的執行緒。
Coroutine 雖然是在執行緒中,但是它不只是單位更小的執行緒,它與執行緒最大的差異就是執行緒是採取搶佔式多工(Preemption multitasking),而 Coroutine 採用的是協作式多工 (Cooperative multitasking),這兩種的差異,我們下一篇文章再來討論。
由上一點我們知道, 每一個執行緒底下都會有多個 Coroutine,當我們需要切換不同的執行緒來執行時, Coroutine 提供了調度器 (Dispatchers) 讓我們能夠切換到不同的執行緒上。
注意到,這邊是採用調度器來進行切換執行緒的動作,也就是說切換不同的執行緒的動作,這個部分是由 Coroutine 自行處理,我們不需要管理執行緒的建立與停止。
在 Android 中,Coroutine 提供了三個調度器
我們可以根據我們的需求切換到適當的調度器中。
使用執行緒時,如果需要把執行緒停止,我們通常會透過一個 flag 來把執行緒停止,如下:
class MyThread(var isRunning: Boolean) {
lateinit var thread: Thread
fun run() {
thread = thread {
var i = 0
while (isRunning) {
println(".")
Thread.sleep(100)
i++
if (i == 10) {
stop()
}
}
}
}
fun stop() {
isRunning = false
}
}
fun main() {
val myThread = MyThread(true)
myThread.run()
}
我們使用了 isRunning
這個 flag 來讓執行緒可以持續執行,當 isRunning=false
時, while(isRunning){}
裡面的內容也不會繼續執行,所以當執行緒裏面的任務結束時,執行緒也會自動結束。
thread 的
stop()
已經被棄用,我們不能夠直接呼叫stop()
停止執行緒,因為這樣會造成 memory-leak。
launch
的回傳值是一個 Job
,我們可以使用 Job
所提供的 cancel()
來讓該 launch
所建立的 Coroutine 給停止下來,如下:
class MyCoroutine {
lateinit var job: Job
suspend fun run() = coroutineScope {
job = launch {
repeat(100) { i ->
println("job: wait $i ...")
delay(500L)
if (i == 10) {
job.cancel()
}
}
}
}
}
到這邊為止,我們知道 Coroutine 的四大特點,是用來解決非同步的程式呼叫流程不易控制、減少佔用執行緒的資源、能夠有能力在不同的執行緒上做切換以及能夠任意的取消 Coroutine。
最重要的是,它是 Kotlin 的親兒子,如果你是 Kotlin 的開發者,那麼在非同步的解決方案上,不仿考慮 Coroutine 吧。
有興趣的讀者歡迎參考:https://coroutine.kotlin.tips/
天瓏書局